Skip to content

async-config-poll: add httpPoll (long-poll) scenario#147

Merged
praagyajain merged 1 commit into
keploy:mainfrom
Aditya-eddy:async-config-poll-httppoll
Jul 16, 2026
Merged

async-config-poll: add httpPoll (long-poll) scenario#147
praagyajain merged 1 commit into
keploy:mainfrom
Aditya-eddy:async-config-poll-httppoll

Conversation

@Aditya-eddy

Copy link
Copy Markdown
Member

What

Extends the existing async-config-poll sample so it can exercise Keploy's httpPoll async-egress lane type (keploy/keploy#4368), in addition to the current periodic-poller scenario.

  • config-stub: POLL_HOLD_SECONDS (default 0) holds a watch=true request open that long before answering — a server-timeout long-poll. Default 0 preserves the current immediate behavior.
  • ConfigWatchService: WATCH_ONCE (default off) opens exactly one watch poll then stops the daemon, so the recording holds a single long poll rather than a stream. Default off preserves the current infinite poller.
  • keploy-httppoll.yml: same lane as keploy.yml but type: httpPoll, so the poll is recorded as kind: HttpPoll with an open-duration (pollDurationMs) and held until its resolve testcase at replay.

All additions are env-gated and backward compatible — the default run is unchanged, so the existing periodic scenario keeps working.

Why

The keploy async_config_poll CI job (in keploy/keploy) is gaining a scenario: [periodic, httppoll] matrix so it validates the new httpPoll long-poll path end-to-end (record → kind: HttpPoll + pollDurationMs → replay held ≥ 1). That job needs these sample hooks. The matching keploy/keploy change is on PR #4368.

Validated

Locally against this sample (jdk8 + MySQL + config-stub, keploy built from keploy#4368):

  • Record: 1 kind: HttpPoll, pollDurationMs: "5027" (~5s hold), single poll.
  • Replay: test exit 0, all tests PASSED, async egress verdict {served: 1, held: 1, shape_flags: 0}.

🤖 Generated with Claude Code

Signed-off-by: Aditya Sharma aditya282003@gmail.com

Extends the sample so it can exercise Keploy's httpPoll async-egress lane
type (keploy#4368), not just the periodic poller:

- config-stub: POLL_HOLD_SECONDS (default 0) holds a watch=true request open
  that long before answering — a server-timeout long-poll. Default 0 keeps
  the current immediate behavior for the periodic scenario.
- ConfigWatchService: WATCH_ONCE (default off) opens exactly one watch poll
  then stops the daemon, so the recording holds a single long poll rather
  than a stream. Default off keeps the current infinite poller.
- keploy-httppoll.yml: same lane as keploy.yml but type: httpPoll, so the
  poll is recorded as kind:HttpPoll with an open-duration (pollDurationMs)
  and held until its resolve testcase at replay.

All additions are env-gated and backward compatible; the default run is
unchanged.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 14:40
Aditya-eddy added a commit to keploy/keploy that referenced this pull request Jul 16, 2026
Adds a scenario: [periodic, httppoll] axis to the async_config_poll job so
the async-egress e2e validates the new httpPoll long-poll lane type, not
just the periodic poller:

- periodic (unchanged): fast poller, stub answers immediately, lane type
  http -> asserts the engine SERVED the polls (served >= 1, no drift).
- httppoll: one WATCH_ONCE poll held open by the stub (POLL_HOLD_SECONDS),
  lane type httpPoll (keploy-httppoll.yml) -> asserts the poll is recorded
  as kind:HttpPoll with a pollDurationMs, and that at replay the engine
  HELD it until its resolve testcase (held >= 1). The >60s hang-watchdog
  exemption is unit-tested (supervisor); this e2e uses a short hold for speed.

TEMP: the samples-java checkout is pinned to the fork branch
(Aditya-eddy/samples-java async-config-poll-httppoll) because the sample
changes are in keploy/samples-java#147, not yet on main. REVERT the checkout
to keploy/samples-java (main) once #147 merges.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@praagyajain
praagyajain merged commit 4b3b4fc into keploy:main Jul 16, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-config-poll sample with an env-gated “httpPoll / long-poll” scenario so Keploy can record/replay the config-watch egress as an HttpPoll (capturing pollDurationMs) in addition to the existing periodic polling behavior.

Changes:

  • Add WATCH_ONCE gating in ConfigWatchService to run a single watch poll for the long-poll scenario.
  • Add POLL_HOLD_SECONDS to config-stub to hold watch=true requests open to simulate a server-timeout long-poll.
  • Add a separate keploy-httppoll.yml config that mirrors keploy.yml but sets the async lane type to httpPoll.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
async-config-poll/src/main/java/com/example/asyncconfig/config/ConfigWatchService.java Adds WATCH_ONCE behavior to stop after a single watch poll for the httpPoll scenario.
async-config-poll/keploy-httppoll.yml Introduces a Keploy config variant using type: httpPoll for the config-watch async lane.
async-config-poll/config-stub/main.go Adds a configurable long-poll hold (POLL_HOLD_SECONDS) for watch=true requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +96 to +98
if (watchOnce) {
break; // single long-poll connection for the httpPoll scenario
}
Comment on lines +48 to +56
if hold > 0 {
// Long-poll: hold the connection open until the server timeout,
// then deliver the next version. Abort if the client disconnects.
select {
case <-time.After(hold):
case <-r.Context().Done():
return
}
}
Aditya-eddy added a commit to keploy/keploy that referenced this pull request Jul 17, 2026
keploy/samples-java#147 (the async-config-poll httpPoll sample hooks) is
merged to main, so the async_config_poll job no longer needs the temporary
fork-branch pin. Restore the checkout to keploy/samples-java (main).

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aditya-eddy added a commit to keploy/keploy that referenced this pull request Jul 17, 2026
…s) (#4368)

* docs: design spec for async HTTP long-poll testcases

Slice 1 of the async-testcase effort: egress HTTP long-poll consumers.
Captures the recording-order-anchored interleave replay model, the
config-lane classifier, per-delivery artifact + side-effect assertion,
and the serialize-across-lanes concurrency decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* docs(async): general async-egress engine design + implementation plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): add AsyncLane model + config.Async section

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): add AsyncParser + AsyncAware capability interfaces

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): transport-agnostic engine (gated ordered serving + shape verdict)

Adds the real Engine implementation: per-lane ordered mock streams built
from async-tagged mocks (sorted by asyncSeq), gated serving via a
completed-testcase counter compared against each mock's anchorPos
(anchorPos=0 at startup is always armed), request-shape verdicts
delegated to the lane's AsyncParser, and keep-alive fallback when no
mock is armed. Removes the Task 2 temporary `type Engine struct{}`
placeholder from async.go now that the real type lives in engine.go.

Engine depends only on *models.Mock, models.AsyncLane, and the
AsyncParser interface (no transport imports), proven by a pluggability
test that drives the same engine with a second, independent fake
parser/lane with zero engine changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): deterministic lane priority + idempotent Load + nil-parser warning

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): HTTP AsyncParser (lane glob, 204 keep-alive, shape via SchemaMatch)

Implements async.AsyncParser + async.AsyncAware on *HTTP:
- MatchesLane: host/path glob (path.Match) against lane.Match.
- EmptyResponse: minimal 204 No Content keep-alive.
- MatchRequestShape: reuses the existing SchemaMatch matcher against the
  single recorded mock. Volatile query params (lane.VolatileParams) are
  stripped from BOTH the live and recorded request before comparison —
  stripping only the recorded side (as a naive reading of the shape would
  suggest) leaves the query key-set counts unequal in SchemaMatch's
  MapsHaveSameKeys check and spuriously fails the match.

Adds asyncEngine *async.Engine field + SetAsyncEngine setter to HTTP struct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* test(async): cover recorded-side URLParams volatile strip in MatchRequestShape

Add TestMatchRequestShapeStripsRecordedURLParams and its negative companion
TestMatchRequestShapeRecordedURLParamsKeyDrift, which build a recorded mock
directly with a populated Spec.HTTPReq.URLParams (unlike httpMock(), which
always leaves URLParams nil). This exercises stripVolatile's recorded-side
`if req.URLParams != nil` branch for the first time, mirroring what
pkg.URLParams(req) populates at record time in production.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): record hook stamps async metadata on lane egress

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): order-independent anchor tie-break + record-time nil-parser warning

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): plumb engine onto Proxy (construct + inject + per-test position advance)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): load complete async corpus at agent StoreMocks seam

Add AsyncMockLoader optional Proxy capability and wire the agent to hand
the async engine the COMPLETE async-mock corpus exactly once per store
path (Engine.Load is run-once). In StoreMocksStream, async mocks are
collected DURING the decode loop (before disk/resident routing) so
per-test async egress parked on disk under strict mock-window is still
loaded; in StoreMocks everything is resident so we collect from params.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): serve async deliveries + keep-alive in HTTP decode path

Extract the matched-mock response serializer into
(*HTTP).buildMockResponseBytes and reuse it in both the existing matched
path and a new async serving branch in decodeHTTP. When a live outbound
request routes to a configured async lane, the transport-agnostic engine
decides what to serve (recorded delivery once armed, or a keep-alive
payload) instead of normal mock matching. The branch mirrors the
telemetry short-circuit: write, read the next poll, continue, using the
errCh <- err / return + ctx.Err() early-return pattern. Adds
liveReqToMock to bridge the matcher's req into a *models.Mock for the
engine. Extraction is behavior-identical; full http suite still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* docs(async): correct Task 6 plan (load at agent StoreMocks seam, split 6a/6b/6c)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): install AsyncRecorder record hook when lanes configured

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): stamp in Before* record hooks so metadata persists

AfterMockInsert fires after mockDB.InsertMock persists the mock, so the async
metadata stamp never reached the recorded YAML. Move stamping to
BeforeMockInsert (mirrors the enterprise obfuscator) and track testcase windows
in AfterTestCaseInsert (TestCase.Name is assigned by InsertTestCase). Caught by
the end-to-end record run against a real long-poll sample app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* docs(async): record Before-hook fix + e2e findings (timing, verdict visibility)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): surface async verdict at replay wind-down

Add Engine.LogReport (fires once via sync.Once) and call it from
SetGracefulShutdown and StopProxyServer so the async verdict
(served / shape_flags / not_exercised) prints at end of replay, with one WARN
per shape drift. Makes the shape-flag verdict user-visible; verified e2e
(mutating a served mock's request method surfaces shape_flags:1 + WARN while
still serving, sync tests still pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* refactor(async): apply cleanup-review findings

- engine: run shape-match outside the mutex (Decide no longer holds the lock
  across the parser call); drop the parallel lanes map in favor of the ordered
  slice; cache seq/anchorPos as ints at Load instead of re-parsing per poll;
  own the first-window rule in Engine.AdvanceWindow (removes Proxy.asyncWindowSeen).
- http: build the volatile-param set once per shape check; move the shared
  buildMockResponseBytes serializer to decode.go (out of the async-only file);
  reuse flakyHeaderNoise() in decode.go's auto-noise setup; flatten the async
  serving branch's triplicated error handling into one closure.
- agent: skip the async-tag scan entirely when the proxy is not an AsyncMockLoader.

Behavior-preserving; unit suites + e2e verdict unchanged (served:1, not_exercised:11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): query + regex lane matching (matchQuery, pathRegex)

Async lanes can now target egress more precisely on the same path:
- matchQuery: require query key=value (e.g. watch=true) so a long-poll variant
  of an endpoint is async while a same-path boot call (watch=false) is not.
- match.pathRegex: a regex alternative to the glob match.path, for URLs a glob
  can't express; reuses pkg/matcher.MatchesAnyRegex (cached, fail-closed).
queryOf follows the package's URLParams-first precedence; MatchesLane uses a
single matched flag instead of restating the configured-criteria list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* refactor(async): address PR review — log levels + capability registration

Apply Copilot review feedback on the async-egress engine:

- proxy.InitIntegrations: register an AsyncParser with the engine even when
  the integration does not implement AsyncAware. Registration was nested
  inside the AsyncAware check, so an AsyncParser-only integration would
  silently never register for routing (LaneFor/Decide).
- engine.Decide: no-parser branch WARN -> DEBUG with a config hint. The
  branch is defensive/near-unreachable (LaneFor only matches when a parser
  exists), so a WARN there is misleading noise.
- engine.LogReport: per-flag shape-drift WARN -> INFO with a remediation
  hint (re-record, or widen the lane's volatileParams / match). Matches the
  Info-level verdict header for one coherent end-of-run summary.
- record.BeforeMockInsert: per-mock unresolved-parser WARN -> DEBUG. The
  root cause is already reported once, loudly, at startup; keeping the
  per-mock repeat at DEBUG avoids flooding real recordings.
- record.ResolveAsyncParsers: unregistered-type and not-an-AsyncParser
  WARN -> ERROR with actionable hints. These are genuine config errors that
  silently defeat intent (mocks never stamped async).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): auto-generate lane names from routing identity

AsyncLane.Name is now optional. When omitted, EffectiveName derives a
deterministic name from the lane's ROUTING identity (type + match +
matchQuery) — stable across record and replay, so it works as the join key
stamped on mocks (MetaAsyncLane) and re-derived by the replay engine.
Caller-supplied names are still honored verbatim.

- models.AsyncLane: Name is now omitempty; add EffectiveName() and the bulk
  WithEffectiveNames(). The derived name excludes volatileParams/notExercised
  (replay-time tuning a user may change between runs, which must not shift the
  join key). A path-derived slug keeps it readable (e.g. http-poll-87288cbd).
- NewAsyncRecorder and NewEngine normalize their lanes through
  WithEffectiveNames, so the record-time stamp and the replay-time lookup use
  the same key. WithEffectiveNames also returns a fresh copy, subsuming the
  manual defensive copy NewEngine used to make.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(async): add async-config-poll e2e job to java_linux

Add a Java-on-Linux CI job that exercises the async-egress engine end to end
against the async-config-poll sample (keploy/samples-java): record the /health
and /rules HTTP tests plus the MySQL + config-service egress, then replay with
the real deps down and assert both the ingress tests PASSED and the engine's
`async egress verdict` (served >= 1, shape_flags == 0 — the volatile "version"
watch param matches cleanly).

- New reusable-workflow job `async_config_poll` in java_linux.yml, modelled on
  the existing tidb/mysql jobs (same guard, download-binary, checkout
  samples-java, upload artifacts). Deviations, each commented: Temurin 8 (the
  sample is Spring Boot 1.5), an added setup-go step for the config stub, and a
  single build/build matrix cell — the released binary can neither stamp nor
  serve async mocks, so the *_latest cells could never validate the feature
  (mirrors tidb-stmt-cache's omission comment).
- New run script test_workflow_scripts/java/async_config_poll/java-linux.sh:
  brings up MySQL (docker compose) with a TCP readiness probe, builds+runs the
  Go config stub, records, then replays and checks the report + async verdict.
  Deliberately does not source update-java.sh (which pins Java 17).

The sample app lives in keploy/samples-java#146; this job goes green once that
merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* docs(async): drop internal design/plan docs from the repo

Remove the superpowers-generated design specs and implementation plan; these
are internal working artifacts and do not belong in keploy/keploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* fix(async): keep async-egress mocks out of the per-test mock mapping

The test↔mock mapping is binned by the agent's SyncMockManager purely on
timestamp-window overlap; it has no async awareness (lanes are known only in
the record service, where the AsyncRecorder hook stamps MetaAsync). So a
background/long-poll egress whose timestamp happened to fall inside a
testcase's request window was being attributed to that testcase's mock set —
even though async mocks are served at replay by the async engine from the
complete corpus and must never be a per-test mock.

Track the tempIDs the hook stamps MetaAsync (asyncMockIDs) and exclude them
when building each test's mapping. The mock-insert goroutine records the async
marker before publishing the tempID to correlationMap, and the mapping
goroutine reads it only after finding the tempID there, so a resolved tempID
always has its async status decided (single writer, single reader, unique
tempIDs — no race).

Extracted the mapping resolution into Recorder.resolveMappingEntries and unit-
tested that async tempIDs overlapping a window are excluded while ordinary
(sync) egress is still mapped.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(async): drop unenforced notExercised knob, add lane diagnostics

The notExercised (skip|fail) lane field was documented and settable but its
value was never read by any code path, so `fail` silently behaved as `skip` —
a misleading knob on a new public config surface. Async polling is
keploy-managed and timing-dependent, so the not-exercised policy is keploy's
to decide, not a user knob: remove the field. The engine still tallies and
logs a not-exercised count (report-only, non-blocking).

Also address two diagnostics raised in review:
- NewEngine logs each configured lane's resolved match criteria at startup,
  so an over-broad glob (which would hijack ordinary sync egress) is visible.
- SetMocks (the non-windowed fallback path, which never advances the async
  window) warns once that only startup-anchored deliveries will arm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Aditya Sharma <aditya282003@gmail.com>

* feat(async): AsyncLane.IsPoll/BaseType + poll metadata keys

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): HttpPoll kind + pollKind registry

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): HttpPoll encode/decode reuses the HTTP wire format

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): register Http->HttpPoll poll kind in the http integration

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): stamp poll metadata + re-kind on poll lanes; resolve parser by base type

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): hold poll deliveries until their resolve testcase (sync.Cond)

Adds asyncEntry.poll (from MetaAsyncPoll), a sync.Cond on the engine
mutex broadcast from AdvanceWindow/OnTestComplete, and a ctx-aware
Decide(ctx, lane, live) that parks a not-yet-armed POLL entry on the
cond until completed reaches its anchorPos or ctx is done (cond.Wait
releases the lock while parked, so AdvanceWindow is never gated by an
outstanding long-poll). Non-poll deliveries keep the pre-existing
immediate keep-alive behavior. Adds a held counter surfaced via
ReportSnapshot.Held and LogReport. Extracts the shape-match/verdict
logic into a decideServe helper and the "no data yet" path into a
nil-safe keepAlive helper, both reused by Decide.

Resolves parsers by lane.BaseType() in both LaneFor and Decide (not
raw lane.Type), so a "httpPoll" lane routes to the parser registered
under "http" — without this a poll lane's live request never matches
in LaneFor and the hold could never engage.

Also fixes the one other pre-existing caller of the old
Decide(lane, live) signature (pkg/agent/proxy/proxy_async_test.go,
predating this change) to pass context.Background(), so
`go test ./...` stays green repo-wide.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(async): re-peek cursor entry on every Decide wake, not just once

A parked poll's cond.Wait woke, then Decide re-read s.mocks[s.cursor]
and served/advanced it without re-checking that entry's own anchorPos.
Under concurrent same-lane Decide calls, another consumer could advance
the cursor while this one was parked, so the waking call would serve a
later, not-yet-armed delivery. Restructure the hold as a re-peek loop
under the lock: every iteration (initial pass and every wake) re-reads
the current cursor entry and re-validates completed >= entry.anchorPos
before serving, closing the gap the pre-Task-6 code never had.

Add TestDecideConcurrentSameLaneServesInOrderByAnchor: two poll
deliveries (anchorPos 1 and 2) on one lane, two concurrent Decide
calls, asserting each resolves only at its own anchor and never early.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address final whole-branch review findings for httpPoll feature

- gofmt pkg/models/async.go (MetaPollDurationMs const-block alignment, CI lint blocker)
- engine.go Decide: read e.parsers[lane.BaseType()] under e.mu.Lock() instead of before it, closing a latent data race with RegisterParser (no behavior change)
- diskmocks.go EligibleForResponseSpill: include models.HttpPoll alongside models.HTTP so poll response bodies are disk-spill eligible, mirroring mockdb/util.go's four switches
- asynchook.go: fix stale ResolveAsyncParsers doc comment (keyed by BaseType(), not Type); note BaseType()'s case-sensitive keying vs IsPoll()'s case-insensitive suffix check
- asynchook_test.go: add TestPollLaneClampsNegativePollDurationToZero and TestAsyncPollMockExcludedFromPerTestMapping, locking the pollDurationMs clamp and the per-test mapping exclusion invariants (both verified via temporary code removal to ensure they actually fail on regression)

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(async): exempt poll-lane connections from the record hang watchdog

A long-poll egress connection makes no byte progress for far longer than
the supervisor's 60s hang budget while the server holds it open. The
watchdog could not tell that apart from a hung parser, so it aborted the
parser and fell through to passthrough before the delivery arrived — the
app still got its response but the poll's mock was never recorded (30s
holds captured, 300s holds silently dropped).

recordV2 now matches each request against the configured async lanes by
request shape (LaneFor) as soon as the request is parsed, and for a
poll-type lane calls the new Session.SuspendWatchdog / Supervisor.
SuspendWatchdog to permanently disarm hang detection on that connection.
Panic and mem-cap protection stay active; only no-progress hang detection
is disabled, and only for connections whose in-flight request routes to a
poll lane.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): address review of poll-lane watchdog exemption

- Latch the suspend once per connection so a keep-alive poll-poll-poll
  connection does not re-parse/re-match every request after the first.
- Document that SuspendWatchdog disarms hang detection for the whole
  connection, not just the in-flight request (bounded keep-alive trade-off).
- Add tests: a suspended supervisor still aborts on a mem-cap breach
  (suspend must not weaken mem-cap protection); malformed request bytes
  return false without panicking.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): /simplify pass on httpPoll — reuse + leaner Decide

- isPollLaneRequest reuses liveReqToMock (the same request->mock shaping the
  replay LaneFor path uses) instead of hand-building a mock; drops the manual
  URLParams loop that truncated multi-valued queries, and gates on a new
  Engine.HasPollLanes() so deployments with no poll lanes skip the re-parse.
- Decide registers the ctx-done wake bridge lazily via context.AfterFunc only
  before it actually parks on cond.Wait, instead of spawning a goroutine +
  channel on every call (immediate-return paths no longer pay for it).
- Drop decideServe's unused context parameter.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(async): cover the httpPoll scenario in async_config_poll e2e

Adds a scenario: [periodic, httppoll] axis to the async_config_poll job so
the async-egress e2e validates the new httpPoll long-poll lane type, not
just the periodic poller:

- periodic (unchanged): fast poller, stub answers immediately, lane type
  http -> asserts the engine SERVED the polls (served >= 1, no drift).
- httppoll: one WATCH_ONCE poll held open by the stub (POLL_HOLD_SECONDS),
  lane type httpPoll (keploy-httppoll.yml) -> asserts the poll is recorded
  as kind:HttpPoll with a pollDurationMs, and that at replay the engine
  HELD it until its resolve testcase (held >= 1). The >60s hang-watchdog
  exemption is unit-tested (supervisor); this e2e uses a short hold for speed.

TEMP: the samples-java checkout is pinned to the fork branch
(Aditya-eddy/samples-java async-config-poll-httppoll) because the sample
changes are in keploy/samples-java#147, not yet on main. REVERT the checkout
to keploy/samples-java (main) once #147 merges.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(async): unpin samples-java checkout now that #147 is merged

keploy/samples-java#147 (the async-config-poll httpPoll sample hooks) is
merged to main, so the async_config_poll job no longer needs the temporary
fork-branch pin. Restore the checkout to keploy/samples-java (main).

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): move async bookkeeping to a typed top-level block

Async-egress data (lane, order, anchor, poll/duration) was stamped into the
owning parser's flat Spec.Metadata, and poll deliveries were re-kinded to a
cosmetic HttpPoll kind. Both mixed the async engine's concern into the parser's
model and taxed every Kind switch.

Introduce models.AsyncMeta and carry it in its own block — Spec.Async in
memory, a kind-agnostic top-level `async:` block on the recorded doc. Its
presence marks a mock as async (replacing the MetaAsync flag); Poll marks a
held long-poll (replacing the HttpPoll kind + pollKind registry). Fields are
typed (seq/anchorPos/pollDurationMs ints, poll bool). A poll mock keeps its
base kind (Http). Following keploy's PostgresV3 precedent (sub-type in Spec,
not Kind).

Removed: HttpPoll kind, MetaAsync* string consts, RegisterPollKind/PollKindFor.
AsyncLane.IsPoll()/BaseType() stay (they describe the config lane type).
Round-trip test (TestAsyncBlockRoundTrips) proves the block survives
encode->decode; the async keys no longer leak into Spec.Metadata.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(async): /simplify pass on the async-block refactor

- Carry the Async envelope field across the two codec.go format converters
  (DocToJSON, jsonDocToYamlDoc) and clone it in Mock.DeepCopy — the async
  field was added to the doc structs but these paths were missed (mirrors how
  Noise is handled).
- Add Mock.IsAsync() and route the 5 duplicated `Spec.Async != nil` checks
  (engine Load, agent collect x2, record mapping) through it (matches the
  HasSpilledResponse predicate precedent).
- ci: the httppoll async_config_poll assertion checked `kind: HttpPoll`, which
  no longer exists — poll mocks stay kind Http, so assert the async block's
  `poll: true` + pollDurationMs instead.
- Rename the local `async` var (shadowed the async package import), fix
  comments referencing removed MetaAsync*/HttpPoll symbols, and add a
  DeepCopy async-isolation test.

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* review(async): address PR Pilot (opus/high) findings

- engine.go LaneFor: read e.parsers under e.mu, mirroring Decide's deliberate
  lock (commit 9dff3ad) so every e.parsers access is guarded — LaneFor is on
  both the replay decode and record isPollLaneRequest paths.
- Lock the JSON storage-format round-trip: TestAsyncBlockRoundTrips now also
  serializes via MarshalDoc(FormatJSON)/UnmarshalDoc and asserts Spec.Async
  survives DocToJSON + jsonDocToYamlDoc (two converters that were originally
  missed), not just the yaml.Node path.
- WarnNonWindowed: note that a testcase-anchored POLL delivery blocks its
  connection until replay teardown on the non-windowed path.
- Fix stale collectAsyncMocks doc comment (Spec.Metadata → Mock.IsAsync).

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Aditya Sharma <aditya282003@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants